Expected the adapter to be 'fresh' while restoring state
Asked Answered
C

10

19

I have a viewpager2 with multiple fragments in FragmentStateAdapter. Whenever I try to open a new fragment and then go back to my current one with viewpager2, I get an exception:

Expected the adapter to be 'fresh' while restoring state.

It seems FragmentStateAdapter is unable to properly restore its state as it is expecting it to be empty.

What could I do to fix this ?

Currycomb answered 18/6, 2019 at 10:9 Comment(2)
Have a look at this speakman.net.nz/blog/2014/02/20/…Muleteer
@Muleteer You googeled the error and picked the first result from Google. The thing is that the link doesn't helpTilly
I
21

it can be fixed by viewPager2.isSaveEnabled = false

Istanbul answered 17/9, 2020 at 11:1 Comment(5)
That worked for me! I have a ViewPager2 that pages Fragments each with their own RecycleView, plus a TabLayout at the top. The ViewPager (old and new versions) were not recreating the Fragments after rotation. This fixed the issue.Sabbatarian
This works partially since we are setting isSaveEnabled to false, viewPager state won't be saved and it will always start with default position which means any fragment coming from back stack will always start from position 0(default).Cherice
Did not work for me. When I tried to navigate back, I am only getting the error "SPAN_EXCLUSIVE_EXCLUSIVE spans cannot have a zero length"Transitive
viewPager2.setSaveEnabled = false didn't fix it for me... @Payam Roozbahani's solution did itSelfdevotion
Working fine with the isSaveEnabled = falseTowner
D
8

I encountered the same problem with ViewPager2. After a lot of efforts on testing different methods this worked for me:

public void onExitOfYourFragment() {
    viewPager2.setAdapter(null);
}

When you come back to the fragment again:

public void onResumeOfYourFragment() {
    viewPager2.setAdapter(yourAdapter);
}
Dantedanton answered 6/7, 2019 at 9:22 Comment(4)
Seems like it's a similar solution, but done in a more proper manner, which allows us to reuse adapter. Good find! :)Currycomb
I can't get this to work. If I set it to null in onDestroyView the mFragments property is still not empty on restoring the adapters state which throws this error.Socrates
This did not work for me, plus it would seem that the error produced by ViewPager2 indicates it does not want to re-use the adapter - so the answer by @Currycomb seems more appropriate.Octans
I surrounded this code with try catch because an exception is being thrown if onCreate method setContentView hasn't been initialized completely. Therefore, the viewPager2 is still null at that time... try{ viewPager2.setAdapter(null); } catch(Exception e){ Log.d(TAG, ""+e.getMessage()); }Selfdevotion
C
7

So my problem was that I was creating my FragmentStateAdapter inside my Fragment class field where it was only created once. So when my onCreateView got called a second time I got this issue. If I recreate adapter on every onCreateView call, it seems to work.

Currycomb answered 18/6, 2019 at 14:57 Comment(6)
It is very bad solution. In the result you will create several instance of adapeter, which make your app slowBurkhart
@GeorgiyChebotarev i have terrible experience of lagging probably due to this. My BottomNavigationView is inside a fragment and one if it's tab contains a ViewPager2 with 3 pages and when i move from another tab of BottomNavigationView to one with ViewPager2 i have a nasty lag. ViewPager2 is also works slower than ViewPager didEmotionalize
@Emotionalize I reverted my code to old-school viewPager. For that case adapter could be reused, and I create it in onCreate-method. Looks not bad. If it will not help you, try to use async layout inflating for inner fragments.Burkhart
@GeorgiyChebotarev, thank you. I will check it out. For now, i will try it with ViewPager2 and async layout inflating and see how it will work. Did you have a noticeable difference in performance after reverting to ViewPager? The time it takes from start of onCreteView(before inflating anything) to onResume after i move to fragment with ViewPager2 is about 80ms which means missing 5 framesEmotionalize
@Thracian, I wanted to use viewPager2, but it has strong bugs for single activity architecture. I can not use it, so I reverted to viewPgaer. No, I don't see the big difference between first and second viewpagers.Burkhart
@Georgiy Chebotarev The worst ofender is that the official doc recommends to use this in place of a fragmentPagerAdapter. The main problem is that the constructor needs a viewLifeCycle which can only be obtained after viewCreated, while the Fragments it stores will undoubtedly survive this cycle. If you even try to circumvent the unnecessary recreation of adapter/Fragments, a 10 second countdown is put in place to Destroy any Fragment not on sight upon backStack reentrance. This is a nightmare.Chilcote
C
5

This adapter/view is useful as a replacement for FragmentStatePagerAdapter.

If what you seek is to preserve the Fragments on re-entrance from the Backstack that would be extremely difficult to achieve with this adapter.

The team placed to many breaks in place to prevent this, only god knows why...

They could have used a self detaching lifeCycle observer, which ability was already foresaw in its code, but nowhere in the android architecture makes use of that ability....

They should have used this unfinished component to listen to the global Fragments lifecycle instead of its viewLifeCycle, from here on, one can scale the listening from the Fragment to the viewLifeCycle. (attach/detach viewLifeCycle observer ON_START/ON_STOP)

Second... even if this is done, the fact that the viewPager itself is built on top of a recyclerView makes it extremely difficult to handle what you would expect from a Fragment's behavior, which is an state of preservation, a one time instantiation, and a well defined lifecycle (controllable/expected destruction).

This adapter is contradictory in its functionality, it checks if the viewPager has already been fed with Fragments, while still requiring a "fresh" adapter on reentrance.

It preserves Fragments on exit to the backStack, while expecting to recreate all of them on reentrance.

The breaks on place to prevent a field instantiated adapter, assuming all other variables are already accounted for a proper viewLifeCycle handling (registering/unregistering & setting and resetting of parameters) are:

@Override
    public final void restoreState(@NonNull Parcelable savedState) {
        if (!mSavedStates.isEmpty() || !mFragments.isEmpty()) {
            throw new IllegalStateException(
                    "Expected the adapter to be 'fresh' while restoring state.");
        }
.....
}

Second break:

@CallSuper
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
    checkArgument(mFragmentMaxLifecycleEnforcer == null);
    mFragmentMaxLifecycleEnforcer = new FragmentMaxLifecycleEnforcer();
    mFragmentMaxLifecycleEnforcer.register(recyclerView);
}

where mFragmentMaxLifecycleEnforcer must be == null on reentrance or it throws an exception in the checkArgument().

Third: A Fragment garbage collector put in place upon reentrance (to the view, from the backstack) that is postDelayed at 10 seconds that attempts to Destroy off screen Fragments, causing memory leaks on all offscreen pages because it kills their respective FragmentManagers that controls their LifeCycle.

private void scheduleGracePeriodEnd() {
    final Handler handler = new Handler(Looper.getMainLooper());
    final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            mIsInGracePeriod = false;
            gcFragments(); // good opportunity to GC
        }
    };

    mLifecycle.addObserver(new LifecycleEventObserver() {
        @Override
        public void onStateChanged(@NonNull LifecycleOwner source,
                @NonNull Lifecycle.Event event) {
            if (event == Lifecycle.Event.ON_DESTROY) {
                handler.removeCallbacks(runnable);
                source.getLifecycle().removeObserver(this);
            }
        }
    });

    handler.postDelayed(runnable, GRACE_WINDOW_TIME_MS);
}

And all of them because of its main culprit: the constructor:

public FragmentStateAdapter(@NonNull FragmentManager fragmentManager,
        @NonNull Lifecycle lifecycle) {
    mFragmentManager = fragmentManager;
    mLifecycle = lifecycle;
    super.setHasStableIds(true);
}
Chilcote answered 18/11, 2020 at 18:0 Comment(1)
I think you are right here and this is a bad deal. I spent some time as well trying to get "back" to the viewpager2 I was looking at after the host fragment's view was destroyed and it doesn't look possible.Tundra
A
3

I've faced the same issue.

After some researching I've came to that it was related to instance of Adapter. When it is created as a lazy property of Fragment it crashes with that error.

So creating Adapter in Fragment::onViewCreated resolves it.

Ayr answered 28/6, 2021 at 9:45 Comment(0)
L
1

I was also getting this java.lang.IllegalStateException: Expected the adapter to be 'fresh' while restoring state. when using ViewPager2 within a Fragment.

It seems the problem was because I was executing mViewPager2.setAdapter(mFragmentStateAdapter); in my onCreateView() method.

I fixed it by moving mViewPager2.setAdapter(mMyFragmentStateAdapter); to my onResume() method.

Lanny answered 28/8, 2019 at 22:27 Comment(0)
L
1

I got this problem after moving new SDK version. (Expected the adapter to be 'fresh' while restoring state)

android:saveEnabled="false" at ViewPager2 can be a quick fix but it may not be what you want.

<androidx.viewpager2.widget.ViewPager2    
android:saveEnabled="false"

Because this simply means your viewPager2 will always come on the first tab when your activity is recreated due to the same reason you are getting this error ( config change and activity recreate).

I wanted users to stay wherever they were So I did not choose this solution.

So I looked in my code a bit. In my case, I found a residual code from early days when I was just learning to create android app.

There was a useless call to onRestoreInstanceState() in MainActivity.onCreate, I just removed that call and it fixed my problem.

In most cases, you should not need to override these methods. If you want to override these , do not forget to call super.onSaveInstanceState / super.onRestoreInstanceState

Important Note from documentation

The default implementation takes care of most of the UI per-instance state for you by calling View.onSaveInstanceState() on each view in the hierarchy that has an id, and by saving the id of the currently focused view (all of which is restored by the default implementation of onRestoreInstanceState(Bundle)). If you override this method to save additional information not captured by each individual view, you will likely want to call through to the default implementation, otherwise be prepared to save all of the state of each view yourself.

Check if the information you want to preview is part of a view that may have an ID. Only those with an ID will be preserved automatically.

If you want to Save the attribute of the state which is not being saved already. Then you override these methods and add your bit.

protected void onSaveInstanceState (Bundle outState)

protected void onRestoreInstanceState (Bundle savedInstanceState)

In latest SDK versions Bundle parameter is not null, so onRestoreInstanceState is called only when a savedStateIsAvailable.

However, OnCreate as well gets savedState Parameter. But it can be null first time, so you need to differentiate between first call and calls later on.

Lard answered 29/5, 2022 at 5:16 Comment(0)
R
0

I solved this problem by testing if it is equal null

if(recyclerView.adapter == null) {recyclerView.adapter = myAdapter}
Rosenblum answered 7/6, 2020 at 15:3 Comment(0)
C
0

I've been struggling with this and none of the previous answers helped.

This may not work for every possible situation, but in my case fragments containing ViewPager2 were fixed and few, and I solved this by doing fragment switch with FragmentTransaction's show() and hide() methods, instead of replace() commonly recommended for this. Apply show() to the active fragment, and hide() to all others. This avoids operations like re-creating views, and restoring state that trigger the problem.

Caceres answered 16/9, 2020 at 7:6 Comment(0)
G
0

Change your fragmentStateAdapter code from

MyPagerAdapter(childFragmentManager: FragmentManager, 
               var fragments: MutableList<Fragment>,
               lifecycle: Lifecycle
) : FragmentStateAdapter(childFragmentManager,lifecycle)

to

MyPagerAdapter(fragment: Fragment, 
               var fragments: MutableList<Fragment>
) : FragmentStateAdapter(fragment)

Note: Here we are removing lifecycle and fragmentManager dependency and fragment state gets restored on back press.

Galway answered 26/12, 2022 at 13:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.